"use strict";
/* Webman AI 主应用 */
const {createApp, reactive} = Vue;
import {fireConfetti, formatDate, xhrOnProgress, copyToClipboard, historySave, historyList, historyGet, historyDelete, speak, getTokenLength, listenLink} from "./util.js?v=3.9.4";;
import {cancelRecord, startRecord, stopRecord} from "./xunfei/index.js?v=3.6.0";

const win = window;
const {$, Push, hljs, markdownit, addEventListener, location, setInterval, setTimeout, XMLHttpRequest,
    localStorage, alert, FormData, document, navigator, scrollTo} = win;

win.ai = createApp({
    beforeMount() {
        this.listenCopyCodes();
        this.initMarkdown();
        this.listenImageClick();
        this.listenUnload();
    },
    data() {
        return {
            module: "chat",
            isMobile: false,
            smallWindow: false,
            roleList: [],
            roleId: 1,
            theme: "light",
            loginUser: {
                username: "",
                nickname: "",
                avatar: "/app/ai/avatar/user.png",
                vip: false,
                vipExpiredAt: "",
                nonVipUseRelax: true
            },
            keyword: "",
            showAddressBook: true,
            box: {
                showContextMenu: false,
                showParams: false,
                showSendMethod: false,
                showRoleInfo: false,
                showMore: false,
                showAiInfo: false,
                showHistory: false,
            },
            setting: {
                defaultModels: {},
                audio: {}
            },
            defaultParams: {
                model: "gpt-3.5-turbo",
                maxTokens: 2000,
                temperature: 0.5,
                contextNum: 5,
            },
            roleInfo: {
                roleId: 0,
                name: "",
                desc: "",
                avatar: "",
                pinned: 0,
                lastTime: 0,
                greeting: "",
                rolePrompt: "",
                model: "gpt-3.5-turbo",
                maxTokens: 2000,
                temperature: 0.5,
                contextNum: 5,
                language: "en_zh",
                speaker: "yingying"
            },
            contextMenu: {top: 0, left: 0, roleId: 0},
            history: [],
            historyKeyword: "",
            sendMethod: "Enter", // Ctrl-Enter
            hoverMessageId: 0,
            isSlidedIn: false,
            isSlidedOut: false,
            isCompiled: true,
            showLoading: false,
            uploadPercent: 0,
            microphoneError: '',
            supportSpeak: true,
            mouseDown: false,
            mouseLeave: false,
            footerHeight: 200,
            showBuyLink: false,
        };
    },
    mounted() {
        addEventListener("resize", this.checkMobile);
        addEventListener("resize", this.setFontSize);
        document.addEventListener('keydown', this.listenRecorderStart);
        document.addEventListener('keyup', this.listenRecorderStop);
        const params = new URLSearchParams(window.location.hash.substring(1));
        const role = params.get('role');
        const module = params.get('module');
        this.checkMobile();
        this.setFontSize();
        this.loadSetting(() => {
            if (module) {
                this.switchModule(module);
            }
        });
        this.loadData();
        this.loadRoles(false, () => {
            if (role) {
                this.switchRoleId(parseInt(role));
                this.scrollToBottom(true, false);
            }
        });
        this.scrollToBottom(true, false);
        this.loadUserInfo(()=>{
            this.listen();
        });
        listenLink();
        this.showBuyLink = win.location.href.includes('bla.cn') || win.location.href.includes('workerman.net');
        if (this.isMobile) {
            this.footerHeight = 120;
        }
    },
    watch: {
        "chat.model": function () {
            this.formatRoles();
        }
    },
    computed: {
        chat() {
            return this.roleList.find(item => item.roleId === this.roleId) || {images:[]};
        },
        filter() {
            return [...this.roleList.filter((item) => {
                return item.status !== 1 && !item.deleted && item.name && item.name.indexOf(this.keyword) !== -1;
            })].sort((a, b) => {
                if (a.pinned !== b.pinned) {
                    return b.pinned - a.pinned;
                }
                if(b.lastTime - a.lastTime) {
                    return b.lastTime - a.lastTime;
                }
                return b.installed - a.installed;
            });
        },
        historyItems() {
            return this.history.filter(item => {
                return item.messages.filter(message => !this.historyKeyword || message.content.indexOf(this.historyKeyword) !== -1).length;
            });
        },
        showShadowLayer() {
            return Object.values(this.box).some(value => value);
        },
        isSmallWindow() {
            return this.smallWindow && !this.isMobile;
        }
    },
    methods: {
        deleteImage(index){
            this.chat.images.splice(index,1);
        },
        loadData() {
            const data = JSON.parse(localStorage.getItem("ai.data") || "{}");
            data.theme = data.theme || $(document.body).attr("data-bs-theme") || "light";
            ["roleId", "api", "roleList", "sendMethod", "smallWindow", "theme"].forEach((name) => {
                if (typeof data[name] !== "undefined") {
                    this[name] = data[name];
                }
            });
            this.toggleTheme(data.theme);
            this.formatRoles(true);
        },
        loadSetting(cb) {
            $.ajax({
                url: "/app/ai/setting",
                success: (res) => {
                    if (res.code) {
                        return alert(res.msg);
                    }
                    this.setting = reactive(res.data);
                    if (cb) {
                        cb(res.data)
                    }
                    this.supportSpeak = res.data.audio.enable_yingying_tts || res.data.audio.enable_gpt_tts;
                }
            });
        },
        loadUserInfo(cb) {
            $.ajax({
                url: "/app/ai/user/info",
                success: (res) => {
                    if (res.code) {
                        return alert(res.msg);
                    }
                    this.loginUser = res.data;
                    if (cb) {
                        cb();
                    }
                }
            });
        },
        loadRoles(reset, cb) {
            $.ajax({
                url: "/app/ai/roles?type=all",
                success: (res) => {
                    if (res.code) {
                        return alert(res.msg);
                    }
                    if (reset) {
                        this.roleList = [];
                    }
                    let roles = res.data;
                    for (let role of this.roleList) {
                        if (role.roleId < 200000000 && !roles.some(item => item.roleId === role.roleId)) {
                            this.roleList = this.roleList.filter(item => item.roleId !== role.roleId);
                        }
                    }
                    roles = roles.filter(role => role.preinstalled);
                    this.roleList = this.roleList.concat(roles.filter(role => !this.roleList.some(chat => chat.roleId === role.roleId)));
                    for (let item of roles) {
                        let role = this.roleList.find(it => it.roleId === item.roleId);
                        if (role.updated_at !== item.updated_at) {
                            for (let key in item) {
                                role[key] = item[key];
                            }
                        }
                    }
                    this.formatRoles(true);
                    this.scrollToBottom(true, false);
                    if (cb) {
                        cb();
                    }
                }
            });
        },
        listen() {
            if (!this.loginUser.apikey) {
                return;
            }
            const https = location.protocol === "https:";
            this.connection = new Push({
                "url": https ? "wss://" + location.hostname : "ws://" + location.hostname + ":3131",
                "app_key": this.loginUser.apikey,
            });
            const channel = this.connection.subscribe(this.loginUser.sid);
            channel.on("sensitive-content", (data) => {
                const chat = this.roleList.find(item => parseInt(item.roleId) === parseInt(data.roleId));
                if (chat) {
                    chat.loading = false;
                    chat.messages = [];
                    chat.messages.push({
                        "id": this.genId(),
                        "type": chat.model,
                        "subtype": "",
                        "role": "assistant",
                        "created": new Date().getTime(),
                        "completed": false,
                        "prompt": "",
                        "content": "检测到敏感内容，对话已经清理",
                    });
                    this.saveData();
                }
            });
        },
        switchModule(name, loadedCallback) {
            this.switchModule.loadedCallback = loadedCallback;
            this.module = name;
            if (this.setting.menus && this.setting.menus[name] && !this.setting.menus[name].urlKeep && this.setting.menus[name].keep) {
                this.setting.menus[name].urlKeep = this.setting.menus[name].url;
            }
            window.location.hash = name !== "chat" ? "module=" + name : "role=" + this.roleId;
            this.hideAll();
        },
        switchRoleId(roleId) {
            if (this.roleId !== roleId) {
                this.speak('');
            }
            this.roleId = roleId;
            this.saveData();
            this.scrollToBottom(true, false);
            if (this.isMobile) {
                this.showAddressBook = false;
                this.isSlidedOut = false;
                this.isSlidedIn = true;
            }
            this.resizeInput();
            this.$refs["input"].focus();
            window.location.hash = "role=" + roleId;
        },
        regenerate(chat, message) {
            chat.messages = chat.messages.filter(item => item.id !== message.id);
            if (chat.messages[chat.messages.length - 1]['content'] === message.prompt) {
                chat.messages.splice(-1, 1);
            }
            this.send(message.prompt);
        },
        createUserMessage(content) {
            const chat = this.chat;
            if (!chat.id) {
                chat.id = new Date().getTime();
                chat.title = Array.isArray(content) ? "[图片]" : content.substring(0, 15);
                chat.time = new Date().getTime();
            }
            this.scrollToBottom(true);
            const userMessageId = this.genId();
            const message = reactive({
                "id": userMessageId,
                "type": "text",
                "role": "user",
                "created": new Date().getTime(),
                "completed": true,
                "content": content
            });
            chat.messages.push(message);
            return message;
        },
        send(content) {
            if (this.chat.dataset) {
                return this.embeddingSearch(content);
            }
            this.sendMessage(content);
        },
        sendMessage(content, context) {
            context = context || this.getContext();
            const chat = this.chat;
            content = content || chat.content;

            const rawContent = content;
            if ((!chat.voice && (content === "" || content === "\n")) || chat.loading) {
                return;
            }
            chat.content = "";
            const model = chat.model;
            this.resizeInput();

            // 只是识别图片时
            if(this.supportGptVision(this.chat.model) && this.chat.images.length) {
                const urls = this.chat.images;
                const result = urls.map(url => {
                    return {
                        type: "image_url",
                        image_url: {url}
                    };
                });
                result.push({ type: "text", text: content});
                content = result;
            }
            this.chat.images = [];

            const userMessage = this.createUserMessage(content);
            const userMessageId = userMessage.id;
            let assistantMessageId = this.genId();
            const modelType = this.getModelType(model);
            const lastMessage = reactive({
                "id": assistantMessageId,
                "type": model,
                "subtype": "",
                "role": "assistant",
                "created": new Date().getTime(),
                "completed": false,
                "prompt": rawContent,
                "content": modelType === "image" ? "生成中..." : "",
                "sendLen": 0,
                "buffer": "",
            });
            chat.messages.push(lastMessage);
            // 每个对话只保留最近100条数据
            chat.messages = chat.messages.slice(-100);
            chat.lastTime = new Date().getTime();
            this.saveData();
            chat.loading = true;
            this.scrollToBottom(true);
            let url= "/app/ai/chat/completions";
            const headers = {"Content-Type": "application/json"};
            headers.version = this.setting.version;
            if (this.chat.rolePrompt) {
                context.unshift({"role": "system", "content": this.chat.rolePrompt});
            }
            context.push({"role": "user", "content": content});
            const messages = context;
            let data= {
                "temperature": chat.temperature || this.defaultParams.temperature,
                "stream": true,
                "messages": messages,
                "prompt": content,
                "model": model,
                "chat_id": chat.id,
                "user_message_id": userMessageId,
                "assistant_message_id": assistantMessageId,
                "role_id": chat.roleId,
                "max_tokens": Number(chat.maxTokens),
            };
            data = JSON.stringify(data);
            let cb = () => {
                $.ajax({
                    url: url,
                    data: data,
                    type: "POST",
                    dataType: "json",
                    headers: headers,
                    complete: (xhr, status) => {
                        let message = this.lastMessage(chat);
                        if (!chat.loading || message.id !== assistantMessageId) {
                            return;
                        }
                        const json = xhr.responseJSON;
                        if (json) {
                            if (!message.retry && json.error) {
                                message.retry = true;
                                message.buffer = message.content = "";
                                cb();
                                return;
                            } else if (json.data && json.data[0] && json.data[0]['url']) {
                                message.content = "![](" + res.data[0]['url'] + ")";
                            }  else {
                                this.handleError(json, message);
                            }
                        }
                        chat.loading = false;
                        lastMessage.completed = true;
                        let leftLen = lastMessage.buffer.length - lastMessage.content.length;
                        lastMessage.sendLen = leftLen > 50 ? Math.ceil(leftLen / 10) : Math.max(5, lastMessage.sendLen);
                        this.saveData();
                        if (this.chat.voice) {
                            this.speak(modelType === "image" ? "图片已经创作完毕，希望你喜欢" : lastMessage.buffer);
                        }
                    },
                    xhr: xhrOnProgress((event) => {
                        let message = this.lastMessage(chat);
                        if (!chat.loading || message.id !== assistantMessageId) {
                            return;
                        }
                        const {responseText} = event.target;
                        try {
                            const json = JSON.parse(responseText);
                            if (json && json.error) {
                                return;
                            }
                        } catch (e) {}
                        const chunks = responseText.split('\n');
                        let content = "";
                        let reasoningContent = "";
                        for (let chunk of chunks) {
                            try {
                                const data = JSON.parse(chunk);
                                content += data.content || (data.error ? data.error.message : '');
                                if (data.choices && data.choices[0] && data.choices[0] && data.choices[0].delta.reasoning_content) {
                                    reasoningContent += data.choices[0].delta.reasoning_content;
                                }
                                if ((data.choices && data.choices[0] && data.choices[0].finish_reason === "content_filter") || (data.promptFeedback && data.promptFeedback.blockReason)) {
                                    content += "因为政策隐私安全问题，此内容无法显示";
                                }
                            } catch (e) {}
                        }
                        reasoningContent = reasoningContent ? "<think>\n" + reasoningContent + "\n</think>\n" : "";
                        message.buffer = reasoningContent + content;
                        message.sendLen = Math.max(Math.ceil(content.length * 100 /(new Date().getTime() - message.created)), 1);
                        if (!message.timer) {
                            message.timer = setInterval(() => {
                                if (!message.completed) {
                                    message.sendLen = message.buffer.length - message.content.length <= 30 ? 1 : message.sendLen;
                                }
                                message.content = message.buffer.substring(0, message.content.length + message.sendLen);
                                this.scrollToBottom(false, false);
                                if (message.id !== assistantMessageId || (message.content.length >= message.buffer.length && message.completed)) {
                                    clearInterval(message.timer);
                                    delete message.timer;
                                    delete message.buffer;
                                    delete message.sendLen;
                                    this.saveData();
                                }
                            }, 100);
                        }
                    })
                });
            }
            cb();
        },
        handleError(data, message) {
            if (typeof data === "string") {
                if (/^(<html|<!doctype)/i.test(data.trim())) {
                    message.type = "text";
                    message.content = "**接口返回错误**\n```html\n" + data.trim() + "\n```";
                    message.error = true;
                }
            } else if (data.error || data.code || data["error_msg"] || data['statusCode']) {
                message.type = "text";
                message.content = data.error ? data.error.message || data.error : data['message'];
                message.error = true;
            }
        },
        embeddingSearch(input) {
            input = input || this.chat.content;
            const message = this.createUserMessage(input);
            this.scrollToBottom(true);
            const dataset = this.chat.dataset;
            this.chat.content = "";
            let messages = this.getContext();
            if (this.chat.rolePrompt) {
                messages.unshift({role: "user", content: this.chat.rolePrompt});
            }
            let data = JSON.stringify({
                "max_tokens": 2000,
                "temperature": 0.1,
                "model": this.chat.model,
                messages
            });
            const success = (content) => {
                const maxContextTokens = 6000;
                $.ajax({
                    url: "/app/ai/embedding/search",
                    data: {content, dataset},
                    type: "post",
                    dataType: "json",
                    success: (res) => {
                        const rolePromptBackup = this.chat.rolePrompt;
                        let rolePrompt = this.chat.rolePrompt;
                        if (res.data) {
                            for (let item of res.data) {
                                if (getTokenLength(item) + getTokenLength(rolePrompt) < maxContextTokens) {
                                    rolePrompt += "\n" + item;
                                }
                            }
                        }
                        this.chat.rolePrompt = rolePrompt;
                        this.deleteMessage(message.id);
                        this.sendMessage(input);
                        this.chat.rolePrompt = rolePromptBackup;
                    }
                });
            };
            if (messages.length < 2) {
                return success(input);
            }
            $.ajax({
                url: "/app/ai/chat/summarize",
                data: data,
                type: "POST",
                dataType: "json",
                headers: {"Content-Type": "application/json"},
                success: (res) => {
                    success(res.error ? input : res.content || input);
                }
            });
        },
        implodeContent(content, original) {
            if (!Array.isArray(content)) {
                return content;
            }
            let result = "";
            for (let item of content) {
                if (item.type === "image_url") {
                    result += original ? item.image_url.url + " " :"![图片]("+item.image_url.url+")\n";
                } else {
                    result += item.text;
                }
            }
            return result;
        },
        getModelType(model) {
            return /(cogview|dall)/.test(model) ? "image" : "chat";
        },
        deleteMessage(id) {
            this.chat.messages = this.chat.messages.filter(message => message.id !== id);
            this.saveData();
        },
        modelSupportImage(model) {
            return this.supportGptVision(model);
        },
        supportGptVision(model) {
            return model.startsWith('gpt-4-vision') || model.startsWith('gpt-4-turbo') || model.startsWith('gpt-4o') || model.startsWith('glm-4v');
        },
        handleDrop(event) {
            if (!this.modelSupportImage(this.chat.model)) {
                return;
            }
            const files = event.dataTransfer.files;
            for (let i = 0; i < Math.min(files.length, 5); i++) {
                const file = files[i];
                if (file.type.startsWith("image")) {
                    this.doUploadImage(file);
                    event.preventDefault();
                }
            }
        },
        handlePaste(event) {
            if (!this.modelSupportImage(this.chat.model)) {
                return;
            }
            const items = event.clipboardData.items;
            for (let i = 0; i < Math.min(items.length, 5); i++) {
                const item = items[i];
                if (item.type.indexOf("image") !== -1) {
                    const file = item.getAsFile();
                    this.doUploadImage(file);
                }
            }
        },
        doUploadImage(file, cb) {
            const formData = new FormData();
            if (file.size > 10 * 1024 * 1024) {
                return alert("单个文件不能大于10M");
            }
            formData.append("image", file);
            $.ajax({
                url: "/app/ai/upload/image",
                type: "POST",
                data: formData,
                processData: false,
                contentType: false,
                xhr: () => {
                    const xhr = new XMLHttpRequest();
                    xhr.upload.addEventListener("progress", (event) => {
                        if (event.lengthComputable) {
                            this.uploadPercent = Math.round((event.loaded / event.total) * 100);
                        }
                    }, false);
                    return xhr;
                },
                success: (res) => {
                    if (cb) {
                        cb(res);
                    }
                    if (res.code) {
                        webman.error(res.msg);
                        return;
                    }
                    let image = location.protocol + "//" + location.host + res.data.url;
                    this.chat.images.push(image);
                }
            });
        },
        openUploadImage() {
            this.$refs["uploadInput"].click();
        },
        uploadImage(event) {
            const file = event.target.files[0];
            this.doUploadImage(file);
            this.$refs["uploadForm"].reset();
        },
        openContextMenu(roleId, event) {
            this.contextMenu.roleId = roleId;
            const winHeight = win.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
            const contextMenuHeight = 110;
            this.contextMenu.top = event.clientY > winHeight - contextMenuHeight ? event.clientY - contextMenuHeight : event.clientY;
            this.contextMenu.left = event.clientX + 5;
            this.box.showContextMenu = true;
            event.preventDefault();
        },
        closeContextMenu() {
            this.contextMenu.roleId = 0;
            this.box.showContextMenu = false;
        },
        formatRoles(reset) {
            for (const chat of this.roleList) {
                if (!chat.messages) {
                    chat.messages = [];
                }
                if (!chat.messages.length && chat.greeting) {
                    chat.messages = [{
                        "role": "assistant",
                        "created": new Date().getTime(),
                        "content": chat.greeting
                    }];
                }
                for (let message of chat.messages) {
                    if (message.choices) {
                        message.content = message.choices[0].delta.content;
                        message.choices = null;
                    }
                }
                chat.content = chat.content || "";
                chat.loading = reset ? false : chat.loading;
                chat.lastTime = chat.lastTime || 0;
                chat.pinned = chat.pinned || 0;
                chat.voice = reset ? false : chat.voice || false;
                chat.images = [];
                if (reset) {
                    for (const message of chat.messages) {
                        message.completed = true;
                    }
                }
            }
        },
        sendMethodSelect(item) {
            this.box.showSendMethod = false;
            this.sendMethod = item;
            this.saveData();
        },
        showPanel(name) {
            this.hideAll();
            this.box["show" + name] = true;
        },
        hideAll() {
            if (this.box.showParams) {
                this.saveData();
            }
            for (let key in this.box) {
                this.box[key] = false;
            }
            this.closeContextMenu();
        },
        showRoleInfoBox() {
            this.clearRoleInfo();
            this.box.showRoleInfo = true;
        },
        clearRoleInfo() {
            this.roleInfo.roleId = 0;
            this.roleInfo.avatar = "/app/ai/avatar/ai.png";
            this.roleInfo.name = this.roleInfo.desc = this.roleInfo.greeting = this.roleInfo.rolePrompt = "";
            this.roleInfo.model = 'gpt-3.5-turbo';
        },
        editRole(roleId) {
            this.clearRoleInfo();
            this.roleInfo.roleId = roleId;
            for (const item of this.roleList) {
                if (item.roleId === roleId) {
                    this.roleInfo = Object.assign({}, item);
                    break;
                }
            }
            this.closeContextMenu();
            this.box.showRoleInfo = true;
        },
        deleteRole(roleId) {
            for (const item of this.roleList) {
                if (item.roleId === roleId) {
                    item.deleted = true;
                    break;
                }
            }
            this.saveData();
            this.closeContextMenu();
            const role = this.roleList.find(role => !role.deleted);
            if (role) {
                this.roleId = role.roleId;
            }
        },
        pinRole(roleId) {
            for (const item of this.roleList) {
                if (item.roleId === roleId) {
                    item.pinned = item.pinned ? 0 : 1;
                    break;
                }
            }
            this.saveData();
            this.closeContextMenu();
        },
        saveRole(roleInfo) {
            this.hideAll();
            const time = new Date().getTime();
            roleInfo = roleInfo || this.roleInfo;
            roleInfo.roleId = roleInfo.roleId || time;
            roleInfo.pinned = 0;
            roleInfo.deleted = false;
            roleInfo.lastTime = new Date().getTime();
            const index = this.roleList.findIndex(item => item.roleId === roleInfo.roleId);
            if (index !== -1) {
                this.roleList[index] = Object.assign({}, this.roleList[index], roleInfo);
                this.saveData();
                return;
            }
            roleInfo.messages = [];
            this.roleList.push(Object.assign({}, roleInfo));
            this.formatRoles();
            this.saveData();
        },
        saveData(key, value) {
            if (key) {
                this[key] = value;
            }
            localStorage.setItem("ai.data", JSON.stringify({
                roleId: this.roleId,
                roleList: this.roleList,
                sendMethod: this.sendMethod,
                smallWindow: this.smallWindow,
                theme: this.theme
            }));
        },
        uploadAvatar() {
            const formdata = new FormData();
            formdata.append("avatar", $("#avatar")[0].files[0]);
            $.ajax({
                url: "/app/ai/upload/avatar",
                type: "post",
                contentType: false,
                processData: false,
                data: formdata,
                success: (res) => {
                    this.roleInfo.avatar = res.data.url;
                }
            });
        },
        cancel(chat) {
            chat = chat || this.chat;
            chat.loading = false;
            let message = this.lastMessage(chat);
            if (message) {
                message.completed = true;
                if (message.timer) {
                    clearInterval(message.timer);
                    delete message.timer;
                    delete message.buffer;
                    delete message.sendLen;
                }
            }
            this.saveData();
        },
        async newChat() {
            this.cancel();
            this.speak('');
            const chat = this.chat;
            if (chat.id) {
                if (!chat.title) {
                    const firstItem = this.chat.messages.find(item => item.role === "user");
                    chat.title = firstItem ? firstItem.content : chat.title;
                }
                await historySave(chat.roleId, chat.id, chat.title, chat.time, chat.messages);
            }
            this.chat.messages = [];
            this.formatRoles();
            fireConfetti();
            this.chat.id = 0;
            this.saveData();
        },
        async showHistory(roleId) {
            this.box.showHistory = !this.box.showHistory;
            if (!this.box.showHistory) {
                return;
            }
            this.history = await historyList(roleId);
        },
        async deleteHistory(roleId, chatId) {
            await historyDelete(roleId, chatId);
            this.history = await historyList(roleId);
        },
        async historyGet(roleId, chatId) {
            const chat = this.chat;
            if (chat.id) {
                await historySave(chat.roleId, chat.id, chat.title, chat.time, chat.messages);
            }
            const {title, messages} = await historyGet(roleId, chatId);
            chat.title = title;
            chat.messages = messages;
            chat.id = chatId;
            this.hideAll();
            this.saveData();
        },
        lastMessage(chat) {
            return chat.messages[chat.messages.length - 1];
        },
        resetSystem() {
            localStorage.clear();
            this.loadRoles(true, () => {
                fireConfetti();
            });
            this.hideAll();
        },
        getContext() {
            let context = [];
            let contextNum = parseInt(this.chat.contextNum || this.defaultParams.contextNum) * 2;
            if (contextNum !== 0) {
                this.chat.messages.slice(-contextNum).forEach((message) => {
                    if (!message.error) {
                        context.push({role: message.role, content: this.removeThink(message.content)});
                    }
                });
            }
            if (!this.supportGptVision(this.chat.model)) {
                for (let item of context) {
                    if (Array.isArray(item.content)) {
                        item.content = this.implodeContent(item.content, true);
                    }
                }
            }
            return context;
        },
        listenUnload() {
            window.addEventListener('beforeunload', (event) => {
                this.saveData();
            });
        },
        listenCopyCodes() {
            $(document).on("click", ".hljs .block-copy", (event) => {
                this.copyToClipboard($(event.target).parent().next().text());
            });
        },
        copyToClipboard(content) {
            if (Array.isArray(content)) {
                content = this.implodeContent(content, true);
            }
            copyToClipboard(content);
        },
        listenImageClick() {
            $(document).on("click", ".message-list .message img", function () {
                let imgUrl = $(this).attr("src");
                const imgExt = imgUrl.split(".").pop();
                if (imgExt.length <= 4) {
                    imgUrl = imgUrl.replace("-sm.", "-lg.");
                }
                ai.previewImage(imgUrl);
            });
            $(document).on("click", ".overlay, .close", function () {
                $(".overlay").hide();
            });
        },
        previewImage(url) {
            this.$refs["image-preview"].src = url;
            this.$refs["image-preview-box"].style["display"] = "flex";
        },
        initMarkdown() {
            this.md = markdownit().set({
                linkify: false,
                breaks: true,
                html: false,
                highlight: function (str, lang) {
                    const header = `<div class="d-flex justify-content-between align-items-center user-select-none" style="margin-top:-10px;margin-right:-8px;"><span class="text-secondary">${lang}</span><span class="block-copy ml-2 icon-btn-secondary"></span></div>`;
                    if (lang && hljs.getLanguage(lang)) {
                        return "<pre class=\"hljs\">" + header + "<code>" + hljs.highlight(str, {language: lang}).value + "</code></pre>";
                    }
                    return "<pre class=\"hljs\">" + header + "<code>" + hljs.highlightAuto(str).value + "</code></pre>";
                }
            }).use(texmath, { engine: katex,
                delimiters: 'dollars',
                katexOptions: { macros: {"\\RR": "\\mathbb{R}"} } } );
        },
        markdown(content) {
            content = (typeof content === "string") || (typeof content === "number") ? content : JSON.stringify(content);
            content = content ? this.replaceThinkTags(content) : '';
            return this.md.render(content);
        },
        replaceThinkTags(text) {
            if (typeof text !== "string") {
                return "";
            }
            if (!text.startsWith('<think>')) {
                return text;
            }
            return text.replace(/<think>\s*<\/think>/, "").replace(/^<think>\n?/, '```思考...\n').replace(/\n?<\/think>/, '\n```\n\n');
        },
        removeThink(text) {
            if (typeof text !== "string") {
                return "";
            }
            return text ? text.replace(/^<think>[\s\S]*?<\/think>\s*/, '') : "";
        },
        handleEnter(event) {
            if (this.sendMethod === "Enter" && !event.ctrlKey && !event.shiftKey) {
                event.preventDefault();
                this.send();
            } else if (this.sendMethod === "Ctrl-Enter" && event.ctrlKey) {
                event.preventDefault();
                this.send();
            } else if (this.sendMethod === "Enter" && event.ctrlKey) {
                event.preventDefault();
                this.chat.content += "\n";
            }
        },
        scrollToBottom(force, smooth) {
            const messageBox = this.$refs["messageBox"];
            const behavior = smooth !== false ? "smooth" : "auto";
            if (force || messageBox.scrollHeight - messageBox.clientHeight <= messageBox.scrollTop + 100) {
                this.$nextTick(() => {
                    messageBox.scrollTo({top: messageBox.scrollHeight, behavior: behavior});
                });
            }
        },
        scrollToTop(smooth) {
            const behavior = smooth !== false ? "smooth" : "auto";
            this.$refs["messageBox"].scrollTo({top: 0, behavior: behavior});
        },
        genId() {
            return new Date().getTime() + String(Math.floor(Math.random() * 1000));
        },
        formatDate(timestamp) {
            return formatDate(timestamp);
        },
        checkMobile() {
            this.isMobile = win.innerWidth <= 768; // 假设小于768px的宽度为移动端
            if (this.isMobile) {
                this.footerHeight = 120;
            } else {
                this.footerHeight = Math.max(200, this.footerHeight);
            }
        },
        handleInputFocus() {
            // 修复苹果浏览器键盘遮挡输入框问题
            const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
            if (!this.isMobile || !isSafari) {
                return;
            }
            setTimeout(() => {
                if (!this.innerHeight) {
                    this.innerHeight = win.innerHeight < 500 ? win.innerHeight : 395;
                } else {
                    this.innerHeight = win.innerHeight < 500 ? win.innerHeight : this.innerHeight;
                }
                scrollTo(0, document.body.scrollHeight - this.innerHeight);
            }, 100);
        },
        slideOut() {
            setTimeout(() => {
                this.showAddressBook = true;
            }, 80);
            this.isSlidedOut = true;
            this.isSlidedIn = false;
        },
        setFontSize() {
            if (!this.isMobile) {
                document.documentElement.style.fontSize = "15px";
                return;
            }
            const screenWidth = win.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
            const baseWidth = 414; // 基准宽度
            const baseFontSize = 17; // 基准字体大小
            const fontSize = Math.min(screenWidth * baseFontSize / baseWidth, baseFontSize);
            document.documentElement.style.fontSize = fontSize + "px";
        },
        toggleTheme(theme) {
            this.theme = theme ? theme : (this.theme === "light" ? "dark" : "light");
            $(document.body).attr("data-bs-theme", this.theme);
            this.saveData('theme', this.theme);
            for (let key in this.setting.menus) {
                $("#" + key).contents().find("body").attr("data-bs-theme", this.theme);
            }
        },
        handleIframeLoaded(module) {
            this.setIframeTheme(module);
            if (this.switchModule.loadedCallback) {
                this.switchModule.loadedCallback();
                this.switchModule.loadedCallback = null;
            }
        },
        setIframeTheme(module) {
            $("#" + module).contents().find("body").attr("data-bs-theme", this.theme);
            $("#" + module).css("display", "block");
        },
        showIframe() {
            $("#" + this.module).css('display', 'block');
        },
        logout() {
            $.ajax({
                url: '/app/user/logout',
                success: () => {
                    this.loadUserInfo();
                    this.switchModule('chat');
                    this.$nextTick(() => {
                        this.switchModule('me');
                    });
                }
            });
            this.hideAll();
        },
        speak(content) {
            speak(content, this.chat.speaker || "yingying");
        },
        toggleVoice() {
            this.chat.voice = !this.chat.voice ? "ended" : false;
            this.microphoneError = "";
            if (this.chat.voice) {
                navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
                    this.toggleVoice.stream = stream;
                    // h5打开麦克风后播放声音会变小，所以要关闭麦克风，需要的时候再打开
                    if (this.isMobile) {
                        stream.getTracks().forEach(track => track.stop());
                    }
                }).catch((err) => {
                    this.microphoneError = {NotAllowedError:"没有麦克风权限",NotFoundError:"未找到麦克风"}[err.name] || err.message
                });
            } else {
                if (this.toggleVoice.stream) {
                    try {
                        this.toggleVoice.stream.getTracks().forEach(track => track.stop())
                    } catch (e) {}
                }
            }
        },
        cancelVoice() {
            if (!this.chat.voice) {
                return;
            }
            this.changeVoiceStatus(this.chat, "cancel");
            cancelRecord();
            if (this.listenRecorderStart.message) {
                this.deleteMessage(this.listenRecorderStart.message.id);
            }
        },
        listenRecorderStart(event) {
            if (!this.chat.voice) {
                return;
            }
            if (event.code === 'Space' && this.chat.voice !== "listening" && this.chat.voice !== "opening") {
                this.startRecord();
            }
        },
        listenRecorderStop(event) {
            if (!this.chat.voice) {
                return;
            }
            if (event.code === 'Escape') {
                return this.cancelVoice();
            }
            if (event.code === 'Space') {
               this.stopRecord();
            }
        },
        onMouseDown() {
            this.mouseDown = true;
            this.startRecord();
        },
        onMouseUp() {
            this.onTouchEnd();
        },
        onTouch(event) {
            this.onTouchMove.x = event.targetTouches[0].pageX;
            this.onTouchMove.y = event.targetTouches[0].pageY;
            this.startRecord();
        },
        onTouchMove(event) {
            this.mouseLeave = Math.abs(this.onTouchMove.x - event.targetTouches[0].pageX) > 80 || Math.abs(this.onTouchMove.y - event.targetTouches[0].pageY) > 40;
        },
        onTouchEnd() {
            this.mouseDown = false;
            if (!this.chat.voice) {
                return;
            }
            if (this.mouseLeave) {
                this.changeVoiceStatus(this.chat, "cancel");
            }
            this.stopRecord();
        },
        voiceBtnTip() {
            if (!this.chat.loading&&this.chat.voice!=='listening') {
                return this.isMobile ? "按住开始说话" : "按住空格开始说话，按Esc取消";
            }
            if (!this.chat.loading&&this.chat.voice==='listening'&&!this.mouseLeave) {
                return "松开发送消息";
            }
            if (!this.chat.loading&&this.chat.voice=='listening'&&this.mouseLeave) {
                return "松开取消发送";
            }
            if (this.chat.loading) {
                return "响应中";
            }
        },
        startRecord() {
            if (this.chat.loading || this.microphoneError) {
                return;
            }
            this.speak('');
            this.mouseLeave = false;
            this.mouseDown = true;
            const chat = this.chat;
            this.changeVoiceStatus(chat, "opening");
            let message = null;
            startRecord({
                language: this.chat.language || "zh_cn",
                start: () => {
                    if (["cancel", "ending"].includes(chat.voice)) {
                        this.changeVoiceStatus(chat, "ended");
                        return;
                    }
                    message = this.createUserMessage('');
                    this.listenRecorderStart.message = message;
                    this.scrollToBottom();
                    this.changeVoiceStatus(chat, "listening");
                },
                progress: (text) => {
                    if (chat.voice === "cancel") {
                        return;
                    }
                    if (message) {
                        message.content = text;
                    }
                    this.scrollToBottom();
                },
                complete: (text) => {
                    if (message) {
                        this.deleteMessage(message.id);
                    }
                    if (chat.voice === "cancel") {
                        this.changeVoiceStatus(chat, "ended");
                        return;
                    }
                    if (text) {
                        this.sendMessage(text);
                    }
                    this.changeVoiceStatus(chat, "ended");
                    this.mouseDown = false;
                },
                stop: () => {},
                error: (text) => {
                    if (message) {
                        this.deleteMessage(message.id);
                    }
                    this.createUserMessage(text);
                    this.speak(text);
                }
            });
        },
        stopRecord() {
            this.mouseDown = false;
            if (this.chat.voice !== "cancel") {
                this.changeVoiceStatus(this.chat, "ending");
            }
            stopRecord();
            if (this.listenRecorderStart.message) {
                const messageId = this.listenRecorderStart.message.id;
                setTimeout(() => {
                    this.deleteMessage(messageId);
                }, 1500)
            }
        },
        changeVoiceStatus(chat, status) {
            chat.voice = status;
        },
        dragEagle(e) {
            let targetDiv = document.getElementsByClassName("footer")[0];
            let targetDivHeight = targetDiv.offsetHeight;
            let startY = e.clientY;
            document.onmousemove =  (e) => {
                e.preventDefault();
                let distY = Math.abs(e.clientY - startY);
                this.footerHeight = e.clientY > startY ? targetDivHeight - distY : targetDivHeight + distY;
                const maxFooterHeight = Math.min(win.innerHeight - 400, 400);
                this.footerHeight = Math.max(200, Math.min(maxFooterHeight, this.footerHeight));
            };
            document.onmouseup = function () {
                document.onmousemove = null;
            };
        },
        resizeInput() {
            if (this.isMobile && !this.chat.voice) {
                this.$nextTick(() => {
                    this.$refs["input"].style.height = "";
                    this.$refs["input"].style.height = Math.min(this.$refs["input"].scrollHeight, 191) + 'px'
                    this.footerHeight = Math.min(parseInt(this.$refs["input"].style.height) + 71, 262);
                });
            }
        }
    }
}).mount("#app");